﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows;
using System.Collections.ObjectModel;
using System.Windows.Media;
using System.Windows.Markup;
using System.Collections.Specialized;
using SDF.Primitives;
using SDF.Pagination;

namespace SDF
{
    [ContentProperty("Rows")]
    public class Table : SmartBlock
    {
        #region Constructor

        public Table()
        {
            CellMatrix = new Dictionary<Point,TableCell>();

            Columns = new TableColumnCollection();
            Columns.CollectionChanged += OnColumnsChanged;

            Rows = new TableRowCollection();
            Rows.ChildAdded += OnRowAdded;
            Rows.ChildRemoved += OnRowRemoved;

            Container = new ContentControl();
            Container.Background = Brushes.Yellow;
            Border = new Border();
            Container.Content = Border;
            Grid = new Grid();
            Border.Child = Grid;

        }

        #endregion

        #region DependencyProperties

        public static readonly DependencyProperty BorderBrushProperty = 
            System.Windows.Documents.Table.BorderBrushProperty.AddOwner(typeof(Table));

        public static readonly DependencyProperty BorderThicknessProperty =
            System.Windows.Documents.Table.BorderThicknessProperty.AddOwner(typeof(Table));

        public static readonly DependencyProperty CellSpacingProperty =
            System.Windows.Documents.Table.CellSpacingProperty.AddOwner(typeof(Table));

        public static readonly DependencyProperty HeaderRowCountProperty = DependencyProperty.Register(
            "HeaderRowCount",
            typeof(int),
            typeof(Table),
            new FrameworkPropertyMetadata(0));

        #endregion

        #region Properties

        protected ContentControl Container { get; private set; }

        protected Border Border { get; private set; }

        protected Grid Grid { get; private set; }

        public TableColumnCollection Columns { get; private set; }

        public TableRowCollection Rows { get; private set; }

        protected Dictionary<Point, TableCell> CellMatrix { get; private set; }

        protected bool IsGridUpToDate { get; private set; }

        public Brush BorderBrush
        {
            get { return (Brush)GetValue(BorderBrushProperty); }
            set { SetValue(BorderBrushProperty, value); }
        }

        public Thickness BorderThickness
        {
            get { return (Thickness)GetValue(BorderThicknessProperty); }
            set { SetValue(BorderThicknessProperty, value); }
        }

        public double CellSpacing
        {
            get { return (double)GetValue(CellSpacingProperty); }
            set { SetValue(CellSpacingProperty, value); }
        }

        public int HeaderRowCount
        {
            get { return (int)GetValue(HeaderRowCountProperty); }
            set { SetValue(HeaderRowCountProperty, value); }
        }

        protected override bool ShouldRenderBackground
        {
            get { return false; }
        }

        #endregion

        #region Visual Children Implementation

        protected override int VisualChildrenCount
        {
            get { return 1; }
        }

        protected override Visual GetVisualChild(int index)
        {
            if (index < 0 || index >= 1)
                throw new ArgumentOutOfRangeException("index");

            return Container;
        }

        #endregion

        #region PropertyChanged

        protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
        {
            base.OnPropertyChanged(e);
            if (e.Property == BorderBrushProperty)
            {
                Border.BorderBrush = this.BorderBrush;
            }
            else if (e.Property == BorderThicknessProperty)
            {
                Border.BorderThickness = this.BorderThickness;
            }
            else if (e.Property == BackgroundProperty)
            {
                Border.Background = this.Background;
            }
        }

        #endregion

        #region Layout Methods

        protected override Size MeasureOverride(Size availableSize)
        {
            if (!IsGridUpToDate)
                RebuildGridMatrix();

            Container.Measure(availableSize);
            return Container.DesiredSize;
        }

        protected override Size ArrangeOverride(Size availableSize)
        {
            Container.Arrange(new Rect(availableSize));
            return availableSize;
        }

        #endregion

        #region Column and Row Event Handlers

        private void OnColumnsChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            IsGridUpToDate = false;
        }

        private void OnRowAdded(object sender, SmartDataObjectCollectionEventArgs<TableRow> e)
        {
            e.Item.Cells.CollectionChanged += OnCellsChanged;
            IsGridUpToDate = false;
        }

        private void OnRowRemoved(object sender, SmartDataObjectCollectionEventArgs<TableRow> e)
        {
            e.Item.Cells.CollectionChanged -= OnCellsChanged;
            IsGridUpToDate = false;
        }

        private void OnCellsChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            IsGridUpToDate = false;
        }

        #endregion

        #region Grid Methods

        public TableCell GetCellAt(int column, int row)
        {
            var point = new Point(column, row);
            if (!CellMatrix.ContainsKey(point))
                return null;

            return CellMatrix[point];
        }


        protected void RebuildGridMatrix()
        {
            CellMatrix.Clear();

            Grid.Children.Clear();
            Grid.RowDefinitions.Clear(); 
            Grid.ColumnDefinitions.Clear();

            bool hasStarColumn = false;
            foreach (var column in Columns)
            {
                var definition = new ColumnDefinition()
                {
                    Width = column.Width //new GridLength(1, GridUnitType.Star)//column.Width
                };
                Grid.ColumnDefinitions.Add(definition);

                if (column.Background != null)
                {
                    var columnBg = new Border()
                    {
                        Background = column.Background
                    };
                    Grid.SetColumn(columnBg, Columns.IndexOf(column));
                    Grid.SetRowSpan(columnBg, Rows.Count);
                    Grid.Children.Add(columnBg);
                }

                if (column.Width.IsStar)
                    hasStarColumn = true;
            }
            if (hasStarColumn)
            {
                Border.HorizontalAlignment = HorizontalAlignment.Stretch;
            }
            else
            {
                Border.HorizontalAlignment = HorizontalAlignment.Center;
            }


            foreach (var row in Rows)
            {
                var definition = new RowDefinition()
                {
                    Height = GridLengthHelper.Convert(row.Height)
                };
                Grid.RowDefinitions.Add(definition);

                if (row.Background != null)
                {
                    var rowBg = new Border()
                    {
                        Background = row.Background
                    };
                    Grid.SetRow(rowBg, Rows.IndexOf(row));
                    Grid.SetColumnSpan(rowBg, Columns.Count);
                    Grid.Children.Add(rowBg);
                }
            }

            for (int rowIndex = 0; rowIndex < Rows.Count; rowIndex++)
            {
                var row = Rows[rowIndex];

                int columnIndex = 0;
                foreach (var cell in row.Cells)
                {
                    var existingCell = GetCellAt(columnIndex, rowIndex);
                    while (existingCell != null)
                    {
                        columnIndex++;
                        existingCell = GetCellAt(columnIndex, rowIndex);
                    }

                    if (columnIndex >= Columns.Count)
                        throw new ArgumentException("The table defines more cells that can fit on row " + rowIndex.ToString());

                    AddCellToGrid(cell, columnIndex, rowIndex);
                }
            }
        }

        private void AddCellToGrid(TableCell cell, int startColumn, int startRow)
        {
            cell.Row = startRow;
            cell.Column = startColumn;
            Grid.Children.Add(cell);

            // Update the margin according to the cellspacing
            cell.Margin = new Thickness(CellSpacing / 2);

            // Update the matrix
            int row = startRow;
            int rowSpan = cell.RowSpan;
            
            while (rowSpan > 0)
            {
                int columnSpan = cell.ColumnSpan;
                int column = startColumn;

                while (columnSpan > 0)
                {
                    var point = new Point(column, row);
                    if (CellMatrix.ContainsKey(point))
                        throw new InvalidOperationException(String.Format("Table already contains a cell at ({0}, {1})", column, row));

                    CellMatrix[point] = cell;

                    columnSpan--;
                    column++;
                }

                rowSpan--;
                row++;
            }
        }

        #endregion

        #region Harvesting

        public override SmartBlock GetEmptyCopy()
        {
            var result = new Table();
            CopyProperties(result);
            result.BorderBrush = this.BorderBrush;
            result.BorderThickness = this.BorderThickness;
            foreach (var originalColumn in this.Columns)
            {
                result.Columns.Add(new TableColumn() 
                { 
                    Width = originalColumn.Width, 
                    Background = originalColumn.Background
                });
            }
            return result;
        }

        public override HarvestResult Harvest(HarvestingContext context, double remainingHeight, SmartBlock previousBlock)
        {
            bool isPartiallyHarvested = IsPartiallyHarvested(context);

            // If we are at the top level and we follow another element,
            // we may need to break the page or column
            if (PageContent.IsDirectChild(this) && (previousBlock != null) && !isPartiallyHarvested)
            {
                if (this.BreakPageBefore)
                    return HarvestResult.BreakPage;

                if (this.BreakColumnBefore)
                    return HarvestResult.BreakColumn;
            }

            // If this is the first part, count the margin
            var harvestedMargin = this.ActualMargin;
            if (isPartiallyHarvested)
                harvestedMargin.Top = 0;
            else if ((previousBlock == null) && double.IsNaN(this.Margin.Top))
                harvestedMargin.Top = 0;
            remainingHeight -= harvestedMargin.Top;

            // Then, let's adjust the borders
            var harvestedBorder = this.BorderThickness;
            if (isPartiallyHarvested)
                harvestedBorder.Top = 0;
            remainingHeight -= harvestedBorder.Top;

            var harvestedTable = this.GetEmptyCopy() as Table;
            var harvestedCellHeights = new Dictionary<TableCell, double>();
            var harvestedRowHeights = new List<double>();

            // Marks the first row to be harvested in this pass...
            int rowOffset = -1;
            bool isHeaderHarvested = false;

            for (int row = 0; row < this.Rows.Count; row++)
            {
                // Skip rows that have already been fully harvested
                if (IsFullyHarvested(row, context))
                    continue;

                if (rowOffset == -1)
                    rowOffset = row;

                // Harvest the header
                if (!isHeaderHarvested && (row > 0) && (HeaderRowCount > 0))
                {
                    rowOffset -= HeaderRowCount;

                    // The header has already been harvested in the original context. By creating a new context, we can harvest it again
                    var context2 = new HarvestingContext();
                    for (int j = 0; j < HeaderRowCount; j++)
                    {
                        remainingHeight = HarvestRow(j, 0, harvestedTable, context2, remainingHeight, harvestedCellHeights, harvestedRowHeights);
                    }
                }
                isHeaderHarvested = true;

                // See if the next row can fit
                if (CheckIfRowFits(row, remainingHeight, context))
                {
                    remainingHeight = HarvestRow(row, rowOffset, harvestedTable, context, remainingHeight, harvestedCellHeights, harvestedRowHeights);
                }
                else
                {
                    break;
                }
            }

            bool breakColumn = false;

            // If finished with the table, apply bottom border and mark as fully harvested
            if (IsFullyHarvested(this.Rows.Count - 1, context))
            {
                harvestedBorder.Bottom = this.BorderThickness.Bottom;
                context.MarkAsFullyHarvested(this);
            }
            else
            {
                // Remove any bottom margin and border, as we haven't yet finished with the table
                harvestedBorder.Bottom = 0;
                harvestedMargin.Bottom = 0;
                breakColumn = true; // Need to break column, because we coulnd't fit the entire table
            }

            harvestedTable.BorderThickness = harvestedBorder;
            harvestedTable.Margin = harvestedMargin;

            return new HarvestResult(harvestedTable, remainingHeight, breakColumn, false);
        }

        public double HarvestRow(int row, int rowOffset, Table table, HarvestingContext context, double remainingHeight,
            Dictionary<TableCell, double> harvestedCellHeights, List<double> harvestedRowHeights)
        {
            var originalRow = this.Rows[row];
            var harvestedRow = new TableRow()
            {
                Height = originalRow.Height,
                Background = originalRow.Background
            };
            table.Rows.Add(harvestedRow);

            remainingHeight = remainingHeight - this.CellSpacing;

            double thisRowHeight = 0.0;
            // It fits, so harvest each cell for this row 
            for (int column = 0; column < this.Columns.Count; column++)
            {
                var cell = GetCellAt(column, row);
                if (cell == null)
                    continue;

                if (!harvestedCellHeights.ContainsKey(cell))
                {
                    var harvestResult = cell.Harvest(context, remainingHeight, null);
                    double cellHeight = remainingHeight - harvestResult.RemainingHeight;
                    harvestedCellHeights.Add(cell, cellHeight);

                    var harvestedCell = harvestResult.HarvestedBlock as TableCell;
                    // Update the starting row and row span (consider the cell might be drawn on another page)
                    int newRowSpan = cell.RowSpan + cell.Row - row;
                    harvestedCell.RowSpan = newRowSpan;
                    harvestedRow.Cells.Add(harvestedCell);
                }

                // If this is the last row for the cell, we need to update the row height
                if (cell.Row + cell.RowSpan - 1 == row)
                {
                    double thisCellHeight = harvestedCellHeights[cell];
                    int index = 1;
                    while (index < cell.RowSpan)
                    {
                        if (harvestedRowHeights.Count >= index)
                        {
                            thisCellHeight -= harvestedRowHeights[harvestedRowHeights.Count - index];
                        }
                        index++;
                    }

                    if (thisCellHeight > thisRowHeight)
                        thisRowHeight = thisCellHeight;
                }
            }
            thisRowHeight += this.CellSpacing;

            harvestedRowHeights.Add(thisRowHeight);
            return remainingHeight - thisRowHeight + this.CellSpacing;
        }

        private bool IsFullyHarvested(int row, HarvestingContext context)
        {
            for (int column = 0; column < this.Columns.Count; column++)
            {
                var cell = GetCellAt(column, row);
                if (!context.IsFullyHarvested(cell))
                    return false;
            }
            return true;
        }

        private bool CheckIfRowFits(int row, double remainingHeight, HarvestingContext context)
        {
            for (int column = 0; column < this.Columns.Count; column++)
            {
                var cell = GetCellAt(column, row);
                if (cell == null)
                    continue;

                // We don't consider minimum height requirements for cells that started in a previous row. 
                // It will either have been harvested already, or we just broke the page so we must 
                // harvest it anyway
                TableCell previousCell = null;
                if (row > 0)
                    previousCell = GetCellAt(column, row - 1);
                if (cell == previousCell)
                    continue;

                // We don't consider the minimum height for cells that have already started to be harvested,
                // as 
                if (context.HasStartedHarvesting(cell))
                    continue;


                double minCellHeight = cell.CalculateMinHeight();
                if (minCellHeight > remainingHeight)
                    return false; // Well, the remaining space we have in this column doesn't fit our row
            }

            return true;
        }

        public override bool IsPartiallyHarvested(HarvestingContext context)
        {
            if (context.IsFullyHarvested(this))
                return false;

            if ((Rows.Count > 0) && (Columns.Count > 0))
            {
                var cell = GetCellAt(0, 0);
                if (context.IsFullyHarvested(cell))
                    return true;

                return cell.IsPartiallyHarvested(context);
            }


            return false;
        }

        #endregion

        #region CalculateMinHeight

        public override double CalculateMinHeight()
        {
            return 0;
        }

        #endregion
    }
}
